/*
 * Unidata Platform Community Edition
 * Copyright (c) 2013-2020, UNIDATA LLC, All rights reserved.
 * This file is part of the Unidata Platform Community Edition software.
 *
 * Unidata Platform Community Edition is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Unidata Platform Community Edition is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 */

package org.unidata.mdm.data.service.segments.relations.merge;

import java.util.Collections;
import java.util.Date;
import java.util.Objects;
import java.util.Set;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.unidata.mdm.core.type.timeline.Timeline;
import org.unidata.mdm.data.context.MergeFromRelationRequestContext;
import org.unidata.mdm.data.context.MergeToRelationRequestContext;
import org.unidata.mdm.data.context.RecordIdentityContext;
import org.unidata.mdm.data.context.RelationFromIdentityContext;
import org.unidata.mdm.data.context.RelationMergeContext;
import org.unidata.mdm.data.context.RelationToIdentityContext;
import org.unidata.mdm.data.dto.MergeRelationDTO;
import org.unidata.mdm.data.exception.DataExceptionIds;
import org.unidata.mdm.data.exception.DataProcessingException;
import org.unidata.mdm.data.module.DataModule;
import org.unidata.mdm.data.service.impl.CommonRecordsComponent;
import org.unidata.mdm.data.service.impl.CommonRelationsComponent;
import org.unidata.mdm.data.service.impl.MergeRelationsComponent;
import org.unidata.mdm.data.type.apply.RelationMergeChangeSet;
import org.unidata.mdm.data.type.data.OriginRelation;
import org.unidata.mdm.data.type.keys.RecordKeys;
import org.unidata.mdm.data.type.keys.RelationKeys;
import org.unidata.mdm.meta.type.RelativeDirection;
import org.unidata.mdm.system.type.pipeline.Start;
import org.unidata.mdm.system.type.runtime.MeasurementPoint;

/**
 * @author Mikhail Mikhailov on Dec 4, 2019
 */
@Component(RelationMergeStartExecutor.SEGMENT_ID)
public class RelationMergeStartExecutor extends Start<RelationMergeContext, MergeRelationDTO> {
    /**
     * This segment ID.
     */
    public static final String SEGMENT_ID = DataModule.MODULE_ID + "[RELATION_MERGE_START]";
    /**
     * Localized message code.
     */
    public static final String SEGMENT_DESCRIPTION = DataModule.MODULE_ID + ".relations.merge.start.description";
    /**
     * Logger.
     */
    private static final Logger LOGGER = LoggerFactory.getLogger(RelationMergeStartExecutor.class);
    /**
     * The CRC I.
     */
    @Autowired
    private CommonRelationsComponent commonRelationsComponent;
    /**
     * The CRC II.
     */
    @Autowired
    private CommonRecordsComponent commonRecordsComponent;
    /**
     * The MRC.
     */
    @Autowired
    private MergeRelationsComponent mergeRelationsComponent;
    /**
     * Constructor.
     */
    public RelationMergeStartExecutor() {
        super(SEGMENT_ID, SEGMENT_DESCRIPTION, RelationMergeContext.class, MergeRelationDTO.class);
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public void start(RelationMergeContext ctx) {
        MeasurementPoint.start();
        try {
            setup(ctx);
        } finally {
            MeasurementPoint.stop();
        }
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public String subject(RelationMergeContext ctx) {
        setup(ctx);
        RelationKeys keys = ctx.relationKeys();
        return keys.getRelationName();
    }

    protected void setup(RelationMergeContext ctx) {

        if (ctx.setUp()) {
            return;
        }

        // 1. Check "must-be-resolved" side, if this is not a relation identity.
        setupDuplicateSideKeys(ctx);

        // 2. Load timeline and proceed
        setupTimelineAndKeys(ctx);

        // 3. Set remaining fields
        setupFields(ctx);

        // 4. Resolve master if required
        setupMasterKeys(ctx);

        // 5. Setup master state if needed
        setupMergeMasterState(ctx);

        ctx.setUp(true);
    }

    protected void setupDuplicateSideKeys(RelationMergeContext iCtx) {

        if (iCtx.isValidRelationKey()) {
            return;
        }

        // Fail merge, if not a valid relation identity and required view side not exists
        RelativeDirection d = iCtx.getDirection();
        if ((d == RelativeDirection.FROM && Objects.isNull(((RelationFromIdentityContext) iCtx).fromKeys()))
         || (d == RelativeDirection.TO && Objects.isNull(((RelationToIdentityContext) iCtx).toKeys()))) {
            String message = "Cannot merge relation! {} side identity was not given.";
            throw new DataProcessingException(message,
                    DataExceptionIds.EX_DATA_RELATIONS_MERGE_REQUIRED_SIDE_NOT_SUPPLIED,
                    d.name());
        }
    }

    protected void setupTimelineAndKeys(RelationMergeContext iCtx) {

        Timeline<OriginRelation> timeline = commonRelationsComponent.ensureAndGetRelationTimeline(iCtx);
        if (Objects.isNull(timeline) || Objects.isNull(timeline.getKeys())) {

            final String message
                = "Relation merge: relation of type [{}] not found by supplied keys - relation etalon id [{}], relation origin id [{}], "
                + "etalon id: [{}], origin id [{}], external id [{}], source system [{}], name [{}]";

            String relationName = iCtx.relationName();
            String relationEtalonKey = iCtx.getRelationEtalonKey();
            String relationOriginKey = iCtx.getRelationOriginKey();
            String recordEtalonKey = iCtx.getEtalonKey();
            String recordOriginKey = iCtx.getOriginKey();
            String recordExternalKey = iCtx.getExternalId();
            String recordSourceSystem = iCtx.getSourceSystem();
            String recordEntityName = iCtx.getEntityName();

            LOGGER.warn(message,
                    relationName,
                    relationEtalonKey,
                    relationOriginKey,
                    recordEtalonKey,
                    recordOriginKey,
                    recordExternalKey,
                    recordSourceSystem,
                    recordEntityName);

            throw new DataProcessingException(message, DataExceptionIds.EX_DATA_RELATIONS_MERGE_NOT_FOUND_BY_SUPPLIED_KEYS,
                    relationName,
                    relationEtalonKey,
                    relationOriginKey,
                    recordEtalonKey,
                    recordOriginKey,
                    recordExternalKey,
                    recordSourceSystem,
                    recordEntityName);
        }
    }

    protected void setupMasterKeys(RelationMergeContext iCtx) {

        if (Objects.nonNull(iCtx.masterKeys())) {
            return;
        }

        RecordIdentityContext master = null;
        RelativeDirection d = iCtx.getDirection();
        if (d == RelativeDirection.FROM) {
            master = ((MergeFromRelationRequestContext) iCtx).getMaster();
        } else {
            master = ((MergeToRelationRequestContext) iCtx).getMaster();
        }

        RecordKeys masterKeys = null;
        if (Objects.nonNull(master)) {
            // Will throw on not-found
            masterKeys = commonRecordsComponent.ensureKeys(master);
        }

        iCtx.masterKeys(masterKeys);
    }

    protected void setupMergeMasterState(RelationMergeContext iCtx) {

        if (Objects.nonNull(iCtx.masterState())) {
            return;
        }

        RelationKeys keys = iCtx.relationKeys();
        RecordKeys masterKeys = iCtx.masterKeys();
        Set<String> fromFilter = iCtx.getDirection() == RelativeDirection.FROM ? Collections.singleton(keys.getRelationName()) : Collections.emptySet();
        Set<String> toFilter = iCtx.getDirection() == RelativeDirection.TO ? Collections.singleton(keys.getRelationName()) : Collections.emptySet();

        iCtx.masterState(mergeRelationsComponent.getMasterState(masterKeys, fromFilter, toFilter));
    }

    protected void setupFields(RelationMergeContext iCtx) {

        Timeline<OriginRelation> timeline = iCtx.currentTimeline();
        RelationKeys keys = timeline.getKeys();

        // Name and type not really needed. Added just for convenience.
        if (Objects.isNull(iCtx.relationKeys())) {
            iCtx.relationKeys(keys);
        }

        iCtx.relationName(keys.getRelationName());
        iCtx.relationType(keys.getRelationType());

        if (Objects.isNull(iCtx.timestamp())) {
            iCtx.timestamp(new Date());
        }

        if (Objects.isNull(iCtx.changeSet())) {
            iCtx.changeSet(new RelationMergeChangeSet());
        }
    }
}
